/* 
 *  Copyright 2012 CodeMagi, Inc.
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.codemagi.servlets.model;

import com.codemagi.login.model.IUser;
import com.codemagi.util.*;
import java.io.Serializable;
import java.util.*;
import org.apache.log4j.Logger;

/**
 * Node is the base class for all node types.
 *
 * @version 1.0
 * @author August Detlefsen for CodeMagi, Inc.
 */
public class Node extends BaseBean implements Serializable, Comparable {

    Logger log = Logger.getLogger(this.getClass());

    private String className  = this.getClass().getName();
    private String ipAddress;

    private Node parent;     //this can be any type of Node

    private IUser createdBy;    //this can be any type of IUser
    private Date createdDate  = new Date();

    private IUser modifiedBy;    //this can be any type of IUser
    private Date modifiedDate = new Date();

    protected ArrayList terms           = new ArrayList();
    protected ArrayList children        = new ArrayList();
    protected ArrayList users           = new ArrayList();

    /**
     * Mapping of allowed class types that can be attached to this one as children.
     *
     * Key: Class Type
     * Value: Allowed # of instances
     */
    protected HashMap allowedChildren = new HashMap();


    // GETTERS AND SETTERS -------------------------------------------

    public void setClassName(String newValue) {
        className = newValue;
    }

    public String getClassName() {
        return className;
    }

    public void setIpAddress(String newValue) {
	ipAddress = newValue;
    }

    public String getIpAddress() {
        return ipAddress;
    }

    public void setParent(Node newValue) {
        parent = newValue;
    }

    public Node getParent() {
        return parent;
    }


    public void setCreatedBy(IUser newValue) {
        createdBy = newValue;
    }

    public IUser getCreatedBy() {
        return createdBy;
    }

    public void setCreatedDate(Date newValue) {
	if (newValue == null) return;  	//no nulls allowed

        createdDate = newValue;
    }

    public Date getCreatedDate() {
        return createdDate;
    }


    public void setModifiedBy(IUser newValue) {
        modifiedBy = newValue;
    }

    public IUser getModifiedBy() {
        return modifiedBy;
    }

    public void setModifiedDate(Date newValue) {
	if (newValue == null) return;  	//no nulls allowed

        modifiedDate = newValue;
    }

    public Date getModifiedDate() {
        return modifiedDate;
    }


    public void setTerms(ArrayList newValue) {
	if (newValue == null) return;  	//no nulls allowed

        terms = newValue;
    }

    public ArrayList getTerms() {
        return terms;
    }

    public void setChildren(ArrayList newValue) {
	if (newValue == null) return;  	//no nulls allowed

        children = newValue;
    }

    public ArrayList getChildren() {
        return children;
    }

    public void setUsers(ArrayList newValue) {
	if (newValue == null) return;  	//no nulls allowed

        users = newValue;
    }

    public ArrayList getUsers() {
        return users;
    }


    //SPECIAL GETTERS AND SETTERS ------------------------------------

    public Integer getParentId() {
	if (parent == null) return null;
	
	return parent.getId();
    }

    public String getParentName() {
        if (parent == null) return "";

        return parent.getName();
    }

    public String getParentClassName() {
        if (parent == null) return "";

        return parent.getClassName();
    }

    public Integer getCreatedById() {
	if (createdBy == null) return null;
	
	return createdBy.getId();
    }

    public String getCreatedByUsername() {
	if (createdBy == null) return "";
	
	return createdBy.getUsername();
    }

    public String getCreatedByName() {
	if (createdBy == null) return "";

        return createdBy.getName();
    }

    public String getCreatedByEmail() {
	if (createdBy == null) return "";

        return createdBy.getEmail();
    }

    public Integer getModifiedById() {
	if (modifiedBy == null) return null;
	
	return modifiedBy.getId();
    }

    public String getModifiedByUsername() {
	if (modifiedBy == null) return "";
	
	return modifiedBy.getUsername();
    }

    public String getModifiedByName() {
	if (modifiedBy == null) return "";

        return modifiedBy.getName();
    }

    public String getModifiedByEmail() {
	if (modifiedBy == null) return "";

        return modifiedBy.getEmail();
    }

    // HELPERS FOR CHILD NODES ---------------------------------------

    /**
     * Adds a Node as a child of this Node. 
     * 
     * NOTE: This will not replace a child Node if it already exists. If you want to replace a child, first 
     * call removeChild(Node oldValue)
     */
    public void addChild(Node newValue) {

	if (newValue == null) return;   //no nulls allowed
	    
	//get the class type and make sure it is allowed
	if (allowedChildren.size() > 0) {
	    Integer allowedCount = (Integer)allowedChildren.get(newValue.getClass());
	    if (allowedCount == 0) return;
	    //TODO Enforce limits 
	}

	//add the child to this Node
	if (!children.contains(newValue) )  children.add(newValue);
    }

    public void removeChild(Node oldValue) {
	log.debug("Attempting to remove child: " + oldValue);
	/*
	Iterator i = children.iterator();
	while (i.hasNext()) {
	    Node n = (Node)i.next();

	    log.debug("   child: " + n + " matches? " + n.equals(oldValue));
	}
	*/
	boolean worked = children.remove(oldValue);
	log.debug("removeChild worked? " + worked);
    }

    /**
     * @deprecated Use public <T extends Node> List<T> getChildren(T type) instead
     */
    public List getChildren(Class type) {
	List output = new ArrayList(children.size());

	Iterator i = children.iterator();
	while (i.hasNext()) {
	    Object child = i.next();

	    if (child.getClass().equals(type) ) output.add(child);
	}

	return output;
    }

    public <T extends Node> List<T> getChildren(T type) {
	List<T> output = new ArrayList<T>(children.size()); 

	Iterator i = children.iterator();
	while (i.hasNext()) {
	    T child = (T)i.next();

	    if (child.getClass().equals(type.getClass()) ) output.add(child);
	    //	    if (child.getClass().isInstance(type)) output.add(child);
	    //	    if (child instanceof classType) output.add(child);
	}

	return output;
    }

    /**
     * Returns the <i>first</i> child of a particular Class for this Node. 
     * 
     * Note: The developer is responsible for the consequences of using 
     * this method if there are multiple children present.
     */
    public Node getChild(Class type) {
	Iterator i = children.iterator();
        while (i.hasNext()) {
            Node child = (Node)i.next();

	    if (child.getClass().equals(type)) return child;
        }
	
	return null;
    }

    /**
     * Returns true if this Node has any children
     */
    public boolean hasChildren() {
	return children.size() > 0;
    }

    /**
     * Returns true if this Node has any children of the specified class
     */
    public boolean hasChildren(Class type) {
	Iterator i = children.iterator();
        while (i.hasNext()) {
            Node child = (Node)i.next();

	    if (child.getClass().equals(type)) return true;
        }
	
	return false;	
    }

    /**
     * Returns true if this Node has the passed Node as a child
     */
    public boolean hasChild(Node input) {
	return children.contains(input);
    }


    //ACCESS CONTROL -------------------------------------------------
    // HELPERS FOR USER NODES ---------------------------------------

    public void addUser(IUser newValue) {
	if (!users.contains(newValue) ) users.add(newValue);	
    }

    public void removeUser(IUser oldValue) {
	users.remove(oldValue);
    }

    /**
     * Returns a comma separated list of the email addresses of this Node's users
     */
    public String getUserEmails() {
	StringBuffer output = new StringBuffer(32 * users.size());

	Iterator i = users.iterator();
	while (i.hasNext()) {
	    IUser user = (IUser)i.next();

	    output.append(user.getEmail());

	    if (i.hasNext()) output.append(", ");
	}

	return output.toString();
    }


    // INTERFACE STUBS --------------------------------------------

    /**
     * Returns true if the passed user is part of this Node's users list. 
     * 
     * Note that this provides automatic ACL through the NavigationController if you implement the Securable interface
     */
    public boolean hasUser(IUser user) {
	if (user == null || user.getId() == null) return false;

	return (user.isSuperuser() || user.equals(createdBy) || users.contains(user));
    }

    /**
     * Returns a plain text String containing text to be added to the search index. 
     * 
     * This stub method returns the node name, and any terms that are applied to this node. 
     * Override this method in subclasses of Node to include class-specific fields.
     *
     * Subclasses of Node that implement the Searchable interface will be automatically indexed when 
     * BaseServlet.setNodeFields is called. 
     */
    public String getSearchText() {
	StringBuffer output = new StringBuffer(256);

	output.append( getName() ).append(" ");

	List<Term> terms = getTerms();
	for (Term term : terms) {
	    output.append( term.getName() ).append(" ");
	}

	return StringUtils.stripHtmlRegex( output.toString().trim() );
    }


    // HELPERS FOR TAXONOMY -----------------------------------------

    /**
     * Returns the Set of IDs of all Terms attached to this Node.
     */
    public Set<Integer> getTermIds() {
	HashSet<Integer> output = new HashSet<Integer>(terms.size());

	Iterator i = terms.iterator();
	while (i.hasNext()) {
	    Term t = (Term)i.next();

	    output.add(t.getId());
	}

	return output;
    }


    /**
     * Returns the Set of IDs of all Terms attached to this Node.
     */
    public Set<Integer> getTermIds(String alias) {
	HashSet<Integer> output = new HashSet<Integer>(terms.size());

	if (Utils.isEmpty(alias)) return output;

	Iterator i = terms.iterator();
	while (i.hasNext()) {
	    Term term = (Term)i.next();

            if (alias.equals(term.getCategory().getAlias())) output.add(term.getId());
	}

	return output;
    }


    /**
     * Returns the Set of <b>names</b> of all Terms attached to this Node.
     */
    public Set<String> getTermNames(String alias) {
	HashSet<String> output = new HashSet<String>(terms.size());

	if (Utils.isEmpty(alias)) return output;

	Iterator i = terms.iterator();
	while (i.hasNext()) {
	    Term term = (Term)i.next();

            if (alias.equals(term.getCategory().getAlias())) output.add(term.getName());
	}

	return output;
    }


    /**
     * Add a term to a Node. If the term's Category does not allow multiple selection, 
     * other Terms in that Category will be removed.
     */ 
    public void addTerm(Term term) {
	if (term != null && !terms.contains(term)) {

	    if (!term.getCategory().allowMultiple()) removeTerms(term.getCategory());
   
	    terms.add(term);
	}
    }

    /**
     * Returns all of the Terms for a particular Category
     */
    public List<Term> getTerms(Category category) {
	List output = (category == null) ? new ArrayList() : getTerms(category.getId());

	return output;
    }

    /**
     * Returns all of the Terms for a particular Category
     */
    public List<Term> getTerms(Integer categoryId) {
	ArrayList output = new ArrayList(terms.size());

        Iterator i = terms.iterator();
        while (i.hasNext()) {
            Term term = (Term)i.next();

            if (term.getCategoryId().equals(categoryId)) output.add(term);
        }

        return output;
    }

    /**
     * Returns all of the Terms for a particular Category
     */
    public List<Term> getTerms(String alias) {
	List<Term> output = new ArrayList<Term>(terms.size());

	if (Utils.isEmpty(alias)) return output;

        Iterator i = terms.iterator();
        while (i.hasNext()) {
            Term term = (Term)i.next();

            if (alias.equals(term.getCategory().getAlias())) output.add(term);
        }

        return output;
    }

    /**
     * Returns the <i>first</i> Term in a Category for this Node. 
     * 
     * Note: The developer is responsible for the consequences of using 
     * this method if there are multiple Terms present.
     */
    public Term getTerm(Integer categoryId) {
	Iterator i = terms.iterator();
        while (i.hasNext()) {
            Term term = (Term)i.next();

            if (term.getCategoryId().equals(categoryId)) return term;
        }
	
	return null;
    }


    /**
     * Returns the <i>first</i> Term in a Category for this Node. 
     * 
     * Note: The developer is responsible for the consequences of using 
     * this method if there are multiple Terms present.
     */
    public Term getTerm(String alias) {
	if (Utils.isEmpty(alias)) return null;

	Iterator i = terms.iterator();
        while (i.hasNext()) {
            Term term = (Term)i.next();

            if (alias.equals(term.getCategory().getAlias())) return term;
        }
	
	return null;
    }

    /**
     * Removes all Terms in a particular Category
     */
    public void removeTerms(Category category) {
	if (category != null && category.getId() != null) {

	    Iterator i = terms.iterator();
	    while (i.hasNext()) {
		Term t = (Term)i.next();

		if (category.equals(t.getCategory())) i.remove();
	    }
	}
    }

    /**
     * Removes all Terms in a particular Category
     */
    public void removeTerms(Integer categoryId) {
	if (categoryId != null) {
	    Category c = new Category();
	    c.setId(categoryId);

	    removeTerms(c);
	}
    }

    /**
     * Removes a Term from this Node
     */
    public void removeTerm(Term term) {
	terms.remove(term);
    }

    /**
     * Returns true if the Node has at least one term in the specified category
     *
     * @param categoryId   The ID of the category to check
     */
    public boolean hasTerm(Integer categoryId) {
        Iterator i = terms.iterator();
        while (i.hasNext()) {
            Term term = (Term)i.next();

            if (term.getCategoryId().equals(categoryId)) return true;
        }

        return false;
    }

    /**
     * Returns true if the Node has at least one term in the specified category
     *
     * @param alias   The alias of the category to check
     */
    public boolean hasTerm(String alias) {

	if (Utils.isEmpty(alias)) return false;

        Iterator i = terms.iterator();
        while (i.hasNext()) {
            Term term = (Term)i.next();

            if (alias.equals(term.getCategory().getAlias())) return true;
        }

        return false;
    }

    /**
     * Implementation of Comparable interface.
     */
    public int compareTo(Object o2) {
	try {
	    Node n2 = (Node)o2;
	    
	    return this.getName().compareTo(n2.getName());
	    
	} catch (Exception e) {
	    //ClassCastException, NullPointerException for example
	    return 0;
	}
    }

}
